Repository navigation
fix: evaluate range pairs, and answer a condition the runtime cannot decide with unknown - #426
YevheniiKotyrlo wants to merge 1 commit into
Conversation
|
Outdated: #426 is now re-written on top of #425, so the two no longer conflict. The original noteCross-referencing #425 and #426, which conflict — flagging it now rather than at merge time. They are independent siblings ( The hazard is in the
Taking #426's arm wholesale therefore un-fixes #425's range container queries. Taking #425's wholesale loses #426's three-valued handling, where The resolution that is correct in both directions evaluates the interval and returns One more line worth keeping whichever way it lands: #426's boolean-context arm uses A two-case test pins both halves, and neither branch passes it alone:
|
|
Outdated: #426 is now re-written on top of #425, so the two no longer conflict. The original noteCorrection to my note above — I undercounted the conflict set, and the omission was the load-bearing file.
A second resolution needs the same care, in Taking #426's arm onto #425's types does not compile — ["!", query.type === "condition" ? query.condition : (["?"] as MediaCondition)]And one more, in |
Device evidence — before / afterUNFIXED — The control and the pink bar paint, the two blue ones do not. A range pair and FIXED — The control and both blue bars paint, the pink one does not. A range pair is its two comparisons, The bars, top to bottom: the control
Android 16 emulator (API 36, GLES on the host GPU), 1140×2400 @ 480dpi, both frames from one run. Keywords in any case, on
|
| query | where | main |
this PR |
|---|---|---|---|
(orientation: LANDSCAPE) |
600x400 screen | no match | match |
not (orientation: LANDSCAPE) |
600x400 screen | match | no match |
(orientation: Landscape) |
400x600 screen | match | no match |
(prefers-color-scheme: DARK) |
dark scheme | no match | match |
@container (orientation: LANDSCAPE) |
400x200 container | no match | match |
@container not (orientation: LANDSCAPE) |
400x200 container | match | no match |
Unknown keywords, a negative resolution or ratio component, and the input features, on main and here
A probe renders each query against the screen or container named, on main and on this head. Chromium 156, Firefox 157 and WebKit 27.2 answer every row but the TV ones and the container or as this PR does, the iOS and Android rows in their touch contexts and the macOS and Mac Catalyst rows on a desktop; MQ5 §7.1 and §7.2 give a touchscreen's pointer as coarse and its hover as none, and a TV's, whose remote is no pointing device, as none for both, which is what Chrome answers on a device with no touchscreen, mouse or stylus (TouchDevice.java); no engine here runs as a TV.
| query | where | main |
this PR |
|---|---|---|---|
(orientation: bogus) |
400x600 screen | match | no match |
not (orientation: bogus) |
600x400 screen | match | no match |
@container not (orientation: bogus) |
400x200 container | match | no match |
(min-resolution: -1dpi) |
a 2x screen | match | no match |
not (max-resolution: -1dpi) |
a 2x screen | match | no match |
not (-1dppx < resolution < 1dppx) |
a 2x screen | match | no match |
@container (aspect-ratio > -1) or (width > 0px) |
400x200 container | match | no match |
(hover: hover) |
iOS or Android | match | no match |
(hover: hover) |
iOS or Android, after a hover event | match | no match |
(hover: none) |
macOS | match | no match |
(hover) |
macOS | no match | match |
(pointer: coarse) |
iOS or Android | no match | match |
(any-pointer: coarse) |
iOS or Android | no match | match |
(pointer: fine) |
macOS | no match | match |
not (pointer: fine) |
macOS | match | no match |
(pointer: fine) |
Mac Catalyst | no match | match |
(hover: hover) |
a TV | match | no match |
(pointer: none) |
a TV | no match | match |
Firefox alone matches the container or, evaluating its known half (Mozilla bug 1874687), where Chromium and WebKit select no container, as this PR does.
Viewport lengths and the features every screen answers alike, on main and here
A probe renders each query on a 600x400 screen, or in a 400x200 container on it, on main and on this head. Chromium 156 and WebKit 27.2 answer every row but the last two as this PR does, and Firefox 157 every row but (color-gamut), which it answers false, and the last. Firefox alone answers video-dynamic-range, and no engine video-color-gamut, which MQ5 §8 gives the values of color-gamut for a screen's video.
| query | where | main |
this PR |
|---|---|---|---|
(width > 50vw) |
600x400 screen | no match | match |
not (width: 100vw) |
600x400 screen | match | no match |
(50vw < width < 150vw) |
600x400 screen | no match | match |
@container (width > 50vw) |
400x200 container | no match | match |
(overflow-block: scroll) |
600x400 screen | no match | match |
(color-gamut: srgb) |
600x400 screen | no match | match |
(color-gamut) |
600x400 screen | no match | match |
(dynamic-range: standard) |
600x400 screen | no match | match |
(forced-colors: none) |
600x400 screen | no match | match |
(video-dynamic-range: standard) |
600x400 screen | no match | match |
(video-color-gamut: srgb) |
600x400 screen | no match | match |
Where a test departs from an engine
Each says so beside its rows: ios is this library's platform type, which the engines read as a type no device has; Firefox resolves env() in a container query and evaluates the known half of an or (Mozilla bug 1874687); Firefox answers the boolean context of color-gamut and dynamic-range false; lightningcss prints a calc(NaN) as 5.10424e38, which the compiler reads as infinity where the engines read 0; react-native-windows reports Windows' high contrast mode through its own AppTheme, which this library does not read, so forced-colors is unknown there; and an em bound, a container's em, a style() query, p3, rec2020 and high are unknown here, as the body's Known limits name.
Evaluation cost — main against this PR
media-evaluation.test.ts evaluates each query a million times after 200,000 of warm-up, seven runs a side per round, each side in a jest process of its own at high priority on the same sixteen logical processors. One warm-up round, then six rounds, the side that goes first swapping each round. ns per evaluation, medians of the rounds with interquartile ranges:
| query | main (2421ea77) |
this PR (fe1a04d1) |
per-round difference |
|---|---|---|---|
(width >= 560px) |
113.5 (106.4–122.9) | 104.3 (100.3–111.7) | −4.4 (−19.7 to −2.1) |
(prefers-color-scheme: dark) |
9.4 (8.8–10.7) | 9.2 (8.8–9.7) | −0.1 (−2.0 to 0.5) |
(width >= 560px) and (width < 800px) |
322.6 (270.4–398.7) | 219.0 (214.0–227.6) | −85.0 (−166.1 to −57.1) |
not ((width >= 9999px) or (orientation: landscape)) |
559.9 (534.1–648.9) | 501.0 (396.4–628.1) | −58.8 (−91.3 to 6.8) |
(400px < width < 800px) |
10.3 (7.4–11.2) | 161.6 (118.8–232.1) | 151.3 (109.0 to 221.4) |
(width > 50vw) |
13.8 (11.7–14.5) | 341.2 (251.1–395.1) | 327.2 (241.3 to 380.8) |
main answers a range pair and a viewport length false without resolving their bounds; here each costs its comparisons. Other work on the machine widened every range. Measured on 519b0c8e, whose emitted JavaScript fe1a04d1 keeps. Windows 11 Pro 10.0.26300, AMD Ryzen 9 9950X3D, Node 26.12.1, jest 29.7.0.
…vewind#426 nativewind#426 answers every media feature in a boolean context from its current value. This branch keeps the one arm its own feature needs.
3d2e1eb to
1b6f6f5
Compare
1b6f6f5 to
a05beba
Compare
460329a to
40bfda2
Compare
1528426 to
3065490
Compare
…decide with unknown Media and container queries now evaluate through one walk and one comparison in three-valued logic (MQ5 §3.1): a term the runtime cannot decide is unknown, its negation too, and a rule applies only when its condition is true, as in Chromium, Firefox and WebKit. - A range pair is its two comparisons, where both runtimes answered it false. - A term is unknown when its feature is unmeasured, its value is outside the feature's syntax (a keyword or a type Media Queries 5 does not give it, env(), a negative resolution or ratio component) or its operand is unresolved, which compiles to null so JSON carries it. A condition the compiler's unknown terms leave no way to be true compiles to no rule, as does a container condition CSS Conditional 5 §5.4 discards. - A number past the largest float compiles to the largest safe integer and a length to the same bound rounded, since JSON writes Infinity as null. - A feature in a boolean context is false only for zero, none and no-preference. - A viewport length in a condition resolves against the window. - hover, any-hover, pointer and any-pointer answer for the primary input, as Safari and Chrome do on a phone; color, color-index, monochrome, grid, scripting, update, overflow-block, overflow-inline, scan, color-gamut, dynamic-range and their video- forms answer as every React Native screen does, forced-colors as none off Windows, and -webkit-device-pixel-ratio as resolution. - A keyword and a media type match in any case. - A compiled condition is readonly at every depth, and the condition compiler reads lightningcss's AST through DeepReadonly.
3065490 to
fe1a04d
Compare


Problem
On
main:(400px < width < 800px)and every other intervalfalse.false, sonot (fictional-feature: 3)and@container not (orientation: bogus)apply everywhere. MQ5 §3.1 makes the term and its negation unknown, and Chromium, Firefox and WebKit apply neither.not ((width: env(safe-area-inset-top)) or (width > 9999px))applies, which no engine does.not (fictional-feature > 10px)applies.(width),(hover),(prefers-color-scheme)(MQ5 §2.4.2).(hover: hover)and(hover: none)both match everywhere, and(pointer: coarse),(overflow-block: scroll)and(color-gamut: srgb)never do.(prefers-color-scheme: DARK)and@media IOSnever match (css-values-4 §4.1, MQ4 §3).(min-resolution: -1dpi)applies on every screen, where css-values-4 §7.4 excludes it and the engines leave the feature unknown.(width > 50vw)never matches andnot (width: 100vw)always does.Solution
evaluateCondition, and one comparison,compareMediaFeature, in three-valued logic (kleene.ts, MQ5 §3.1). A rule applies only when its condition istrue; one the compiler's unknown terms leave no way to be true compiles to no rule.env(), a negative resolution or ratio component — or when the compiler cannot resolve its operand, which compiles tonullsoJSON.stringifycarries it. A number past the largest float compiles to the largest safe integer for the same reason.style()query compiles to an unknown term.noneandno-preferenceare false and every other value true.color8;color-index,monochromeandgrid0;scriptingenabled;updatefast;overflow-blockandoverflow-inlinescroll;scanmatching neither of its values;forced-colorsnonebut on Windows (§12.4); andcolor-gamut,dynamic-rangeand theirvideo-formssrgbandstandard, withp3,rec2020andhighunknown (MQ5 §6.4, §6.5, §8).-webkit-device-pixel-ratioanswers asresolution(Compatibility Standard). An<mq-boolean>compiles to its integer.hover,any-hover,pointerandany-pointeranswer for the primary input (MQ5 §7.1, §7.2), as Safari and Chrome do on a phone: iOS and Androidnoneandcoarse, a TVnoneto both, Mac Catalyst and other platformshoverandfine. Tailwind'shover:then applies where the primary input hovers.DeepReadonly.I changed #451's
container-rejectionexpectation thatnot ((width > 100px) and style(--theme: dark))applies at no width: at 50px the conjunction is false whateverstyle()answers, so its negation applies, as in all three engines.Tests
compiler/condition-semantics.test.ts: what each condition the native tests render compiles to, a value of another type, a non-finite operand, a condition lightningcss's types do not list, and the compiler's pruning against the runtime's answers;condition-types.test.tschecks the readonly types.native/condition-semantics.test.tsx: each condition rendered on a screen or in a container, per platform where it differs, in memory and after a JSON round trip;compare.test.ts,kleene.test.tsandunits.test.tsxcover the shared helpers.vendor/tailwind/states.test.tsxandgroups.test.tsx:hover:andgroup-hover:on Mac Catalyst and on a touchscreen. These and bothcontainer-queries.test.tsxassert the expected colour absent, astoHaveStyle(undefined)matched any style.main, 250 of the 610 cases in the twocondition-semanticsfiles andcontainer-rejection.test.tsxfail.Verification
Every
ci.ymljob passes on this head merged intomain:yarn testat 1995 passed on Ubuntu with Node 22.18.0, and the iOS dev build on Xcode 27.1, where CI pins 26.6. Every conditionmainevaluates costs the same or less. The runs, the engines' answers and the device evidence: this comment.Known limits
(min-width)as(width)(parcel-bundler/lightningcss#1366), drops thenotof an equality range, sonot (width = 100vw)holds on every screen, and prints acalc(NaN)as an overflowing number.emin a media query and aremwithinlineRemoff are fix(compiler): read a media query's em and rem against the font size rem inlines #494's, a container'sinline-sizeandblock-sizefix(native): measure inline-size and block-size container queries #480's, a ratio fix: evaluate aspect-ratio media and container queries #425's, a heldnullcolor scheme fix(native): make colorScheme.set move every reader of the scheme #415's and reduced motion feat(native): evaluate prefers-reduced-motion so motion-reduce / motion-safe work on native #388's. Anemin a container query, a math function, astyle()query, the axes acontainer-typeallows,forced-colorson Windows, which react-native-windows'AppThemereports,prefers-contrast,prefers-reduced-transparency,inverted-colors,device-width,device-heightand the viewport lengths butvwandvhare each a change of its own.Platform.ios.js,Platform.android.js), so an attached mouse or stylus goes unseen, which Safari and Chrome read.isMacCatalystis set at build time (RCTPlatform.mm), so an iPad app on a Mac answers as a touchscreen, and react-native-windows'isTVis always false (Platform.windows.js), so an Xbox answershoverandfine.PlatformnorAccessibilityInforeports a screen's gamut or dynamic range or data saving, sop3,rec2020,highandprefers-reduced-dataare unknown.Merge order
This shares lines with #388, #412, #415, #459, #463, #472, #480 and #492; whichever lands second, I re-cut. #425 and #494 build on this one, and
src/utilities/ascii-case.tsis #447's file byte for byte.#493's error recovery hands the media compiler feature values outside their syntax, which this answers as unknown.